package com.xlongwei.accounts.controller;

import java.time.LocalDate;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;
import org.noear.snack.ONode;
import org.noear.solon.Solon;
import org.noear.solon.Utils;
import org.noear.solon.annotation.Controller;
import org.noear.solon.annotation.Get;
import org.noear.solon.annotation.Inject;
import org.noear.solon.annotation.Mapping;
import org.noear.solon.annotation.Param;
import org.noear.solon.annotation.Post;
import org.noear.solon.cache.jedis.RedisCacheService;
import org.noear.solon.core.handle.Result;
import org.noear.solon.data.cache.CacheService;
import org.noear.solon.data.cache.LocalCacheService;
import org.noear.solon.validation.annotation.Min;
import org.noear.solon.validation.annotation.NotBlank;
import org.noear.solon.validation.annotation.NotNull;
import org.noear.solon.validation.annotation.Pattern;
import org.noear.solon.validation.annotation.Valid;
import org.slf4j.LoggerFactory;

import com.xlongwei.accounts.config.AppConfig;
import com.xlongwei.accounts.config.AppConfig.DsCacheService;
import com.xlongwei.accounts.config.DsConfig;
import com.xlongwei.accounts.config.MySqlUtils;
import com.xlongwei.accounts.dao.UserDao;
import com.xlongwei.accounts.dao.VipDao;
import com.xlongwei.accounts.model.User;
import com.xlongwei.accounts.model.Vip;
import com.xlongwei.accounts.service.VipService;
import ch.qos.logback.classic.Level;
import ch.qos.logback.classic.LoggerContext;
import cn.dev33.satoken.annotation.SaCheckLogin;
import cn.dev33.satoken.secure.SaSecureUtil;
import cn.dev33.satoken.stp.StpUtil;
import lombok.extern.slf4j.Slf4j;

@Valid
@Slf4j
@Controller
@Mapping("user")
public class UserController {
    public static final String PATTERN_PASS = "^(\\S{1,6})||(\\S{64})$";
    public static final String PATTERN_NAME = "^\\S{1,7}$";
    public static final String PATTERN_MOBILE = "^1\\d{10}$";
    public static final String PATTERN_MOBILE_OR_ZERO = "(^1\\d{10}$)||0";
    @Inject
    UserDao userDao;
    @Inject
    DsConfig dsConfig;
    @Inject
    DsCacheService dsCacheService;
    @Inject
    VipService vipService;
    @Inject
    VipDao vipDao;
    @Inject
    MySqlUtils sqlUtils;

    @Post
    @Mapping("login")
    public Result<?> login(@NotBlank @Pattern(PATTERN_MOBILE) String mobile,
            @NotBlank @Pattern(PATTERN_PASS) String pass) {
        if (StpUtil.isLogin()) {
            StpUtil.logout(StpUtil.getLoginId());
        }
        String dsName = MySqlUtils.dsName();
        Long mobileL = Long.parseLong(mobile);
        User user = (Utils.isBlank(dsName) || MySqlUtils.dsNames.containsKey(dsName)) ? userDao.findByMobile(mobileL) : null;
        if ((user != null && (user.getLevel() <= 0 || !validPass(pass, user))) || (user == null && pass.length() == 64)) {
            return Result.failure("登录失败");
        }
        boolean newUser = user == null;
        if (newUser) {
            if (Utils.isBlank(dsName) || AppConfig.allowNewUserDsNames.contains(dsName)) {
                user = new User();
                user.setMobile(mobileL);
                user.setPass(pass);
                user.setLevel(2);
                Integer userid = userDao.create(user);
                user.setUserid(userid);
                doVip(mobile, dsName, true, null, null, null, null);
            } else if(mobile.equals(dsName)) {
                boolean add = dsConfig.add(mobile, "2" + mobile.substring(1), pass);
                if(add) {
                    user = userDao.findByMobile(mobileL);
                    doVip(mobile, mobile, true, null, null, null, null);
                } else {
                    return Result.failure("登录失败");
                }
            } else {
                return Result.failure("登录失败");
            }
        }
        String id = MySqlUtils.dsNameSuffix(user.getUserid());
        StpUtil.login(id);
        vipService.set(user);
        if (newUser) {
            return Result.succeed(user);
        }
        ONode node = node(user);
        return Result.succeed(node);
    }

    private boolean validPass(String pass, User user) {
        if (pass.length() == 64) {// sha256
            String hash = user.getPass();
            hash = SaSecureUtil.sha256(hash) + hash;
            LocalDate dt = LocalDate.now();
            int rounds = rounds(dt); // 根据月+日+周变化（与前端一致），每天使用不同hash值登录
            for (int i = 0; i < rounds; i++) {
                hash = (dt.getMonthValue() - 1) + SaSecureUtil.sha256(hash) + dt.getDayOfMonth();
            }
            hash = SaSecureUtil.sha256(hash);
            return Objects.equals(pass, hash);
        } else {
            return Objects.equals(pass, user.getPass());
        }
    }

    public static int rounds(LocalDate dt) {
        int m = dt.getMonthValue(), d = dt.getDayOfMonth(), w = dt.getDayOfWeek().getValue();
        return 1 + (((m - 1) + d + (w == 7 ? 0 : w)) % 13) % 7;
    }

    private ONode node(User user) {
        ONode node = ONode.load(user);
        List<User> partners = userDao.findByPartner(user.getMobile());
        if (partners.isEmpty()) {
            node.set("partner", 0L);
        } else {
            User partner = null;
            for (User item : partners) {
                if (user.getPartner().equals(item.getMobile())) {
                    partner = item;
                }
            }
            partner = partner != null ? partner : partners.get(0);
            node.set("partner", partner.getUserid());
        }
        return node;
    }

    @Post
    @Mapping("checkLogin")
    public Result<?> checkLogin(@Pattern(PATTERN_MOBILE) String mobile) {
        boolean isLogin = StpUtil.isLogin();
        if (isLogin) {
            Integer userid = userid();
            User user = userDao.retrieve(userid);
            vipService.set(user);
            ONode node = node(user);
            return Result.succeed(node);
        } else {
            List<String> list = vipService.ds(mobile);
            if (list.size() > 1) {
                list = list.stream().filter(ds -> MySqlUtils.dsNames.containsKey(ds) || mobile.equals(ds))
                        .collect(Collectors.toList());
            }
            ONode node = new ONode();
            node.set("tenants", list);
            node.set("newUser", list.size() == 0 || (list.size() == 1 && !MySqlUtils.dsNames.containsKey(list.get(0))));
            node.set("friends", Solon.cfg().getBool("friendship.enable", true) ? Solon.cfg().toBean("friendship.links", ArrayList.class) : Collections.emptyMap());
            return Result.succeed(node);
        }
    }

    @Post
    @SaCheckLogin
    @Mapping("changePass")
    public Result<?> changePass(@NotBlank String pass,
            @NotBlank @Pattern(PATTERN_PASS) String newPass) {
        Integer userid = userid();
        User user = userDao.retrieve(userid);
        if (user != null && Objects.equals(pass, user.getPass())) {
            user.setPass(newPass);
            userDao.update(user);
            return Result.succeed(null, "改密成功");
        }
        return Result.failure("改密失败");
    }

    @Post
    @SaCheckLogin
    @Mapping("changePartner")
    public Result<?> changePartner(@Pattern(PATTERN_MOBILE) String mobile) {
        Integer userid = userid();
        User user = userDao.retrieve(userid);
        if (!Utils.isBlank(mobile)) {
            Long mobileL = Long.parseLong(mobile);
            user.setPartner(mobileL.equals(user.getMobile()) ? 0L : mobileL);
            userDao.update(user);
            vip(mobile, MySqlUtils.dsName(), false, null, null, user.getPartner().toString(), null);
        }
        return Result.succeed(user);
    }

    @Post
    @Mapping("newDs")
    public Result<?> newDs(@NotBlank @Pattern(PATTERN_MOBILE) String mobile,
            @NotBlank @Pattern(PATTERN_MOBILE) String partner, @NotBlank @Pattern(PATTERN_PASS) String pass, String ds) {
        if(partner.equals(mobile)) partner = "2" + mobile.substring(1);
        boolean add = Utils.isBlank(ds) ? dsConfig.add(mobile, partner, pass) : false;
        if(Utils.isNotBlank(ds) && AppConfig.allowNewUserDsNames.contains(ds)) {
            Long mobileL = Long.parseLong(mobile), partnerL = Long.parseLong(partner);
            User mobileU = userDao.findByMobile(mobileL), partnerU = userDao.findByMobile(partnerL);
            if(mobileU == null && partnerU == null) {
                User user = new User();
                user.setMobile(mobileL);
                user.setPass(pass);
                user.setLevel(2);
                user.setUserid(userDao.create(user));
                user.setPartner(partnerL);
                userDao.update(user);
                // add partner
                user.setUserid(null);
                user.setMobile(partnerL);
                user.setUserid(userDao.create(user));
                user.setPartner(mobileL);
                userDao.update(user);
                add = true;
            }
        }
        if (add) {
            doVip(mobile, mobile, true, null, null, null, null);
            doVip(partner, mobile, true, null, null, null, null);
            doVip(mobile, mobile, false, null, null, partner, null);
            doVip(partner, mobile, false, null, null, mobile, null);
        }
        return Result.succeed(add);
    }

    @Post
    @SaCheckLogin
    @Mapping("vip")
    public Result<?> vip(@NotBlank @Pattern(PATTERN_MOBILE) String mobile, String ds,
            boolean newUser, LocalDate regDay, LocalDate vipDay, @Pattern(PATTERN_MOBILE_OR_ZERO) String partner,
            @Pattern(PATTERN_MOBILE_OR_ZERO) String inviter) {
        Vip vip = AppConfig.isSuperAdmin() ? doVip(mobile, ds, newUser, regDay, vipDay, partner, inviter) : null;
        return Result.succeed(vip);
    }

    private Vip doVip(String mobile, String ds, boolean newUser, LocalDate regDay, LocalDate vipDay,
            String partner, String inviter) {
        String mobileDs = mobile + (Utils.isBlank(ds) ? "" : "@" + ds);
        Vip vip = vipService.vip(mobileDs, newUser, regDay);
        if (newUser == false && vip != null) {
            boolean update = vipDay != null || regDay != null || Utils.isNotBlank(partner) || Utils.isNotBlank(inviter);
            log.info("update={} regDay={} vipDay={} partner={} inviter={}", update, vip.getRegDay(), vip.getVipDay(),
                    vip.getPartner(), vip.getInviter());
            vip.setRegDay(regDay == null ? vip.getRegDay() : regDay);
            vip.setVipDay(vipDay == null ? vip.getVipDay() : vipDay);
            vip.setPartner(Utils.isBlank(partner) ? vip.getPartner() : Long.parseLong(partner));
            vip.setInviter(Utils.isBlank(inviter) ? vip.getInviter() : Long.parseLong(inviter));
            if (update) {
                vipDao.update(vip);
            }
        }
        return vip;
    }

    @Get
    @Post
    @SaCheckLogin
    @Mapping("list")
    public Result<?> list(LocalDate now, Integer days) {
        if (AppConfig.isSuperAdmin() && (now != null || days != null)) {
            List<Vip> list = vipService.list(now, days);
            return Result.succeed(list);
        }
        List<User> list = isAdmin() ? userDao.list() : Collections.emptyList();
        return Result.succeed(list);
    }

    @Post
    @SaCheckLogin
    @Mapping("update")
    public Result<?> update(@NotNull @Min(1) Integer userid, @Pattern(PATTERN_NAME) String pass,
            Integer level, @Min(1) Long partner) {
        User user = isAdmin() ? userDao.retrieve(userid) : null;
        if (user != null && (Utils.isNotBlank(pass) || level != null || partner != null)) {
            user.setPass(Utils.isBlank(pass) ? user.getPass() : pass);
            user.setLevel(level != null ? level : user.getLevel());
            user.setPartner(partner != null ? partner : user.getPartner());
            userDao.update(user);
        }
        return Result.succeed(user);
    }

    @Post
    @Mapping("log")
    public Object log(@Param("logger") String loggerName, @Param("level") String levelName,
            @Param("token") String tokenValue) {
        LoggerContext lc = (LoggerContext) LoggerFactory.getILoggerFactory();
        List<ch.qos.logback.classic.Logger> loggers = null;
        if (!Utils.isBlank(loggerName)) {
            ch.qos.logback.classic.Logger logger = lc.getLogger(loggerName);
            if (logger != null) {
                loggers = Arrays.asList(logger);
                String token = System.getProperty("logserver.token");
                if (!Utils.isBlank(levelName) && (Utils.isBlank(token) || token.equals(tokenValue))) {
                    Level level = Level.toLevel(levelName, null);
                    log.warn("change logger:{} level from:{} to:{}", logger.getName(), logger.getLevel(), level);
                    logger.setLevel(level);
                }
            }
        }
        if (loggers == null) {
            loggers = lc.getLoggerList();
        }
        log.info("check logger level, loggers:{}", loggers.size());
        List<Map<String, String>> list = loggers.stream().sorted((a, b) -> a.getName().compareTo(b.getName()))
                .map(logger -> {
                    Map<String, String> m = new HashMap<>();
                    m.put("logger", logger.getName());
                    m.put("level", Objects.toString(logger.getLevel(), ""));
                    return m;
                }).collect(Collectors.toList());
        return ONode.load(Collections.singletonMap("loggers", list));
    }

    @Post
    @SaCheckLogin
    @Mapping("clearCache")
    public Result<?> clearCache(String ds) {
        if (AppConfig.enableCache && Utils.isNotBlank(ds) && AppConfig.isSuperAdmin()) {
            CacheService cacheService = dsCacheService.cacheService(ds);
            log.info("ds={} cache={}", ds, cacheService);
            if (cacheService instanceof LocalCacheService) {
                ((LocalCacheService) cacheService).clear();
            } else if (cacheService instanceof RedisCacheService) {
                ((RedisCacheService) cacheService).client().getBucket().removeByPattern(ds + "*");
            }
            return Result.succeed();
        }
        return Result.failure();
    }

    @Post
    @SaCheckLogin
    @Mapping("admin")
    public Result<?> admin(String ds, String sql) {
        if (!AppConfig.isSuperAdmin() || Utils.isBlank(sql))
            return Result.failure();
        if (Utils.isBlank(ds)) {
            Map<String, Object> map = new HashMap<>();
            MySqlUtils.dsNames.forEach((k, v) -> {
                try {
                    MySqlUtils.dsName.set(k);
                    Object obj = sqlUtils.sql(sql).queryRowList(Map.class);
                    map.put(k, obj);
                } finally {
                    MySqlUtils.dsName.remove();
                }
            });
            return Result.succeed(map);
        } else {
            Object obj = sqlUtils.sql(sql).queryRowList(Map.class);
            return Result.succeed(obj);
        }
    }

    @Post
    @SaCheckLogin
    @Mapping("logout")
    public Result<?> logout() {
        StpUtil.logout(StpUtil.getLoginId());
        return Result.succeed(null, "登出成功");
    }

    public static Integer userid() {
        String id = StpUtil.getLoginId().toString();
        int idx = id.indexOf('@');
        return Integer.parseInt(idx == -1 ? id : id.substring(0, idx));
    }

    public static boolean isAdmin() {
        return 1 == userid();
    }
}
